[Сервер] Конфигурация Nginx с показом IP посетителя и без показа IP. Проект Новый год с таймером

Шаг 1. Установка Nginx

sudo apt-get purge nginx nginx-common nginx-full -y
sudo apt-get autoremove -y
sudo rm -rf /etc/nginx
sudo apt update
sudo apt install nginx -y

Шаг 2. Создание и настройка конфигурационного файла

Открыть файл конфигурации для сайта:

sudo nano /etc/nginx/sites-available/mysite.conf

Вставить следующий конфиг:

##
## NEWYEAR.TONICMAN.RU — FULL WORKING CONFIG
##

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name newyear.tonicman.ru;

    root /var/www/html;
    index index.html;

    location /myip {
        default_type text/plain;
        return 200 "$remote_addr";
    }

    location / {
        gzip off;

        sub_filter_once on;

        sub_filter
          'И готов к демонстрации ✨'
          'Ваш IP: $remote_addr';

        try_files $uri $uri/ =404;
    }   

    gzip on;
    gzip_min_length 256;
    gzip_types
        text/css
        application/javascript
        application/json
        application/xml
        image/svg+xml;

    ssl_certificate     /etc/letsencrypt/live/newyear.tonicman.ru/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/newyear.tonicman.ru/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;
}

server {
    listen 80;
    listen [::]:80;

    server_name newyear.tonicman.ru;

    return 301 https://$host$request_uri;
}

Либо конфигурация без IP и "И готов к демонстрации ":

##
## NEWYEAR.TONICMAN.RU — FULL WORKING CONFIG
##

# -------------------------
# HTTPS
# -------------------------
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name newyear.tonicman.ru;

    root /var/www/html;
    index index.html;

    # -------------------------
    # Route: /myip (optional, no JS conflict)
    # -------------------------
    location /myip {
        default_type text/plain;
        return 200 "$remote_addr";
    }

    # -------------------------
    # Main site
    # -------------------------
    location / {
    gzip off;

   try_files $uri $uri/ =404;
}   
    # -------------------------
    # gzip for assets only
    # -------------------------
    gzip on;
    gzip_min_length 256;
    gzip_types
        text/css
        application/javascript
        application/json
        application/xml
        image/svg+xml;

    # -------------------------
    # SSL (Certbot)
    # -------------------------
    ssl_certificate     /etc/letsencrypt/live/newyear.tonicman.ru/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/newyear.tonicman.ru/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;
}

# -------------------------
# HTTP → HTTPS redirect
# -------------------------
server {
    listen 80;
    listen [::]:80;

    server_name newyear.tonicman.ru;

    return 301 https://$host$request_uri;
}

Шаг 3. Активация конфигурации

Создать символическую ссылку в sites-enabled:

sudo ln -s /etc/nginx/sites-available/mysite.conf /etc/nginx/sites-enabled/

Шаг 4. Проверка конфигурации и перезапуск сервера

Проверить конфиг:

sudo nginx -t

Если команда выдала:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

то ошибок нет.

Перезагружаем Nginx:

sudo systemctl reload nginx

Шаг 5. (Опционально) Установка SSL-сертификата через Certbot

Установка certbot через snap

Удалить старый certbot, если он установлен:

sudo apt remove certbot

Установить certbot в snap:

sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Получение сертификата:

sudo certbot --nginx

Шаблон index.html положить в

/var/www/html/

Код шаблона

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>🎄 Сервер онлайн</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />

<style>
body {
  margin: 0;
  min-height: 220vh;
  font-family: 'Segoe UI', Tahoma, sans-serif;
  background: radial-gradient(circle at top, #0b2a55, #000814 70%);
  color: #fff;
  text-align: center;
  overflow-x: hidden;
}

/* ===== ВЕРХНИЕ ГИРЛЯНДЫ ===== */
#garlandsTop {
  position: fixed;
  top: 0; left: 0;
  width: 100%;
  height: 200px;
  pointer-events: none;
  z-index: 50;
  transition: none;
}

/* Вертикальная гирлянда для смартфонов */
@media (max-width: 480px) {
  #garlandsTop {
    width: 80px !important;
    height: 100vh !important;
    top: 0;
    left: 0;
  }
  .hero {
    padding-left: 100px;
  }
  .time {
    gap: 10px;
    flex-wrap: wrap;
    justify-content: center;
  }
  .time span {
    min-width: 45%;
    margin-bottom: 6px;
  }
  .time b {
    font-size: 24px;
  }
  .time small {
    font-size: 10px;
  }
}

/* Заголовок */
h1 {
  font-size: clamp(36px, 6vw, 64px);
  margin: 0;
  background: linear-gradient(90deg, #fff, #ffd86b, #fff);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  text-shadow: 0 0 40px rgba(255, 215, 120, 0.5);
}

.sub {
  margin-top: 14px;
  font-size: clamp(22px, 3vw, 32px);
  background: linear-gradient(90deg, #cce3ff, #fff, #cce3ff);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  text-shadow: 0 0 35px rgba(120, 180, 255, 0.6);
}

/* ===== ТАЙМЕР ===== */
.countdown {
  margin-top: 28px;
  font-size: 18px;
  opacity: 0.95;
}
.time {
  display: flex;
  justify-content: center;
  gap: 22px;
  margin-top: 10px;
}
.time span {
  display: flex;
  flex-direction: column;
  min-width: 72px;
}
.time b {
  font-size: 34px;
  background: linear-gradient(90deg, #fff, #ffd86b, #fff);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}
.time small { font-size: 12px; opacity: 0.75; }

/* ===== ЁЛКА ===== */
.canvas-wrap {
  margin: 130px auto 0;
  width: 440px;
  height: 560px;
}
#tree {
  width: 100%;
  height: 100%;
  cursor: pointer;
}

/* ===== СНЕГ ===== */
.snow {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 1;
}
.snow span {
  position: absolute;
  top: -10px;
  width: 6px; height: 6px;
  background: #fff;
  border-radius: 50%;
  animation: fall linear infinite;
}
@keyframes fall {
  to { transform: translateY(240vh); }
}
@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
  }
}
</style>
</head>

<body>

<canvas id="garlandsTop"></canvas>

<div class="hero">
  <h1>Привет! Сервер работает.</h1>

  <!-- ⚠️ НЕ МЕНЯТЬ — ДЛЯ IP -->
  <div class="sub">И готов к демонстрации ✨</div>

  <div class="countdown">
    До Нового года осталось:
    <div class="time">
      <span><b id="d">0</b><small>дней</small></span>
      <span><b id="h">0</b><small>часов</small></span>
      <span><b id="m">0</b><small>мин</small></span>
      <span><b id="s">0</b><small>сек</small></span>
    </div>
  </div>

  <div class="canvas-wrap">
    <canvas id="tree" width="440" height="560"></canvas>
  </div>
</div>

<div class="snow" id="snow"></div>

<script>
document.addEventListener('DOMContentLoaded', () => {
  const gCanvas = document.getElementById('garlandsTop');
  if (!gCanvas || !gCanvas.getContext) {
    if (gCanvas) {
      gCanvas.outerHTML = "<p style='color:#fff; text-align:center; padding:20px;'>Ваш браузер не поддерживает canvas. Обновите браузер для корректного отображения.</p>";
    }
    return;
  }

  const SNOW_COUNT = 170;
  const FIREWORK_CHANCE = 0.05;

  /* ===== СНЕГ ===== */
  const snow = document.getElementById('snow');
  for (let i = 0; i < SNOW_COUNT; i++) {
    const s = document.createElement('span');
    s.style.left = Math.random() * 100 + 'vw';
    s.style.animationDuration = 10 + Math.random() * 15 + 's';
    s.style.transform = `scale(${Math.random() + 0.3})`;
    snow.appendChild(s);
  }

  /* ===== ФЕЙЕРВЕРК ===== */
  class Firework {
    constructor(x, y, ctx) {
      this.x = x;
      this.y = y;
      this.ctx = ctx;
      this.particles = [];
      for (let i = 0; i < 100; i++) {
        this.particles.push({
          x: this.x,
          y: this.y,
          vx: (Math.random() - 0.5) * 5,
          vy: (Math.random() - 0.5) * 5,
          alpha: 1,
          radius: 2 + Math.random() * 2,
        });
      }
    }
    update() {
      this.particles.forEach(p => {
        p.x += p.vx;
        p.y += p.vy;
        p.vy += 0.05;
        p.alpha -= 0.015;
      });
      this.particles = this.particles.filter(p => p.alpha > 0);
    }
    draw() {
      const ctx = this.ctx;
      this.particles.forEach(p => {
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
        ctx.fillStyle = `rgba(255,${Math.floor(200 * p.alpha)},0,${p.alpha})`;
        ctx.shadowColor = `rgba(255,${Math.floor(200 * p.alpha)},0,${p.alpha})`;
        ctx.shadowBlur = 10;
        ctx.fill();
      });
    }
    done() {
      return this.particles.length === 0;
    }
  }

  function showFireworks() {
    const cw = window.innerWidth;
    const ch = window.innerHeight;
    const canvas = document.createElement('canvas');
    canvas.style.position = 'fixed';
    canvas.style.left = 0;
    canvas.style.top = 0;
    canvas.width = cw;
    canvas.height = ch;
    canvas.style.zIndex = 1000;
    document.body.appendChild(canvas);
    const ctx = canvas.getContext('2d');

    const fireworks = [];
    function loop() {
      ctx.clearRect(0, 0, cw, ch);
      if (Math.random() < FIREWORK_CHANCE) {
        fireworks.push(new Firework(Math.random() * cw, Math.random() * ch / 2, ctx));
      }
      fireworks.forEach(fw => {
        fw.update();
        fw.draw();
      });
      for (let i = fireworks.length - 1; i >= 0; i--) {
        if (fireworks[i].done()) {
          fireworks.splice(i, 1);
        }
      }
      requestAnimationFrame(loop);
    }
    loop();
  }

  /* ===== ТАЙМЕР ===== */
  const d = document.getElementById('d');
  const h = document.getElementById('h');
  const m = document.getElementById('m');
  const s = document.getElementById('s');

  function nextNewYear() {
    const now = new Date();
    let y = now.getFullYear();
    if (now >= new Date(y, 0, 1)) y++;
    return new Date(y, 0, 1, 0, 0, 0);
  }

  const eventTime = nextNewYear();

  function updateTimer() {
    const diff = eventTime - new Date();
    if (diff <= 0) {
      document.querySelector('.countdown').innerHTML = '<h2 style="font-size: 48px; color: gold; text-shadow: 0 0 10px #ff0, 0 0 20px #ff0;">С Новым годом!</h2>';
      clearInterval(timerInterval);
      showFireworks();
      return;
    }
    d.textContent = Math.floor(diff / 86400000);
    h.textContent = Math.floor(diff / 3600000) % 24;
    m.textContent = Math.floor(diff / 60000) % 60;
    s.textContent = (Math.floor(diff / 1000) % 60).toString().padStart(2, '0');
  }

  const timerInterval = setInterval(updateTimer, 1000);
  updateTimer();

  /* ===== ГИРЛЯНДА (CANVAS) ===== */
  const gCtx = gCanvas.getContext('2d');
  let isVertical = window.matchMedia("(max-width:480px)").matches;

  function resizeGarlands() {
    if (isVertical) {
      gCanvas.width = 80;
      gCanvas.height = window.innerHeight;
    } else {
      gCanvas.width = window.innerWidth;
      gCanvas.height = 200;
    }
  }
  resizeGarlands();

  let resizeTimeout;
  window.addEventListener('resize', () => {
    clearTimeout(resizeTimeout);
    resizeTimeout = setTimeout(() => {
      isVertical = window.matchMedia("(max-width:480px)").matches;
      resizeGarlands();
    }, 150);
  });

  const lightsCount = 80;
  const topLights = new Array(lightsCount).fill(0).map((_, i) => ({
    t: i / lightsCount,
    hue: Math.random() * 360
  }));
  const verticalLights = new Array(lightsCount).fill(0).map((_, i) => ({
    t: i / lightsCount,
    hue: Math.random() * 360,
    phase: Math.random() * Math.PI * 2
  }));

  function drawHorizontalGarland(t) {
    gCtx.clearRect(0, 0, gCanvas.width, gCanvas.height);
    const baseY = gCanvas.height / 2;
    const amplitude = 25;
    gCtx.strokeStyle = 'rgba(255,255,255,0.25)';
    gCtx.lineWidth = 3;
    gCtx.beginPath();
    for (let x = 0; x <= gCanvas.width; x += 20) {
      const y = baseY + Math.sin(x * 0.01) * amplitude;
      gCtx.lineTo(x, y);
    }
    gCtx.stroke();

    topLights.forEach((l, i) => {
      const x = l.t * gCanvas.width;
      const y = baseY + Math.sin(x * 0.01) * amplitude;
      const wave = (Math.sin(t * 0.003 + i * 0.4) + 1) / 2;

      gCtx.beginPath();
      gCtx.fillStyle = `hsla(${l.hue}, 100%, 70%, ${0.4 + wave * 0.6})`;
      gCtx.shadowColor = `hsla(${l.hue}, 100%, 70%, 1)`;
      gCtx.shadowBlur = 15 + wave * 25;
      gCtx.arc(x, y, 7 + wave * 3, 0, Math.PI * 2);
      gCtx.fill();
    });
  }

  function drawVerticalGarland(t) {
    gCtx.clearRect(0, 0, gCanvas.width, gCanvas.height);
    const baseX = gCanvas.width / 2;
    const amplitude = 15;
    gCtx.strokeStyle = 'rgba(255,255,255,0.25)';
    gCtx.lineWidth = 3;
    gCtx.beginPath();
    for (let y = 0; y <= gCanvas.height; y += 20) {
      const x = baseX + Math.sin(y * 0.02 + t * 0.002) * amplitude;
      gCtx.lineTo(x, y);
    }
    gCtx.stroke();

    verticalLights.forEach((l, i) => {
      const y = l.t * gCanvas.height;
      const x = baseX + Math.sin(y * 0.02 + t * 0.002 + l.phase) * amplitude;
      const wave = (Math.sin(t * 0.005 + i * 0.6) + 1) / 2;

      gCtx.beginPath();
      gCtx.fillStyle = `hsla(${l.hue}, 100%, 70%, ${0.5 + wave * 0.5})`;
      gCtx.shadowColor = `hsla(${l.hue}, 100%, 70%, 1)`;
      gCtx.shadowBlur = 15 + wave * 20;
      gCtx.arc(x, y, 8 + wave * 4, 0, Math.PI * 2);
      gCtx.fill();
    });
  }

  /* ===== ЁЛКА (CANVAS) ===== */
  const tCanvas = document.getElementById('tree');
  const tCtx = tCanvas.getContext('2d');

  const branchPositions = [];
  for (let y = 60; y < 440; y += 6) {
    const w = ((y - 60) / 380) * 180;
    for (let i = 0; i < 6; i++) {
      branchPositions.push({
        x: 220 + (Math.random() - 0.5) * w,
        y: y,
      });
    }
  }

  const treeLights = new Array(45).fill(0).map(() => ({
    x: 220 + (Math.random() - 0.5) * 160,
    y: 100 + Math.random() * 300,
    phase: Math.random() * Math.PI * 2
  }));

  let clickWave = 0;
  tCanvas.addEventListener('click', () => clickWave = 1);

  function drawTree(t) {
    tCtx.clearRect(0, 0, 440, 560);

    tCtx.fillStyle = 'hsl(145,45%,25%)';
    branchPositions.forEach(({x, y}) => {
      tCtx.fillRect(x, y, 2, 8);
    });

    treeLights.forEach(l => {
      const glow = (Math.sin(t * 0.003 + l.phase) + 1) / 2 + clickWave;

      tCtx.save();
      tCtx.fillStyle = `rgba(255,220,150,${0.4 + glow * 0.5})`;
      tCtx.shadowColor = 'rgba(255,220,150,1)';
      tCtx.shadowBlur = 10 + glow * 20;
      tCtx.beginPath();
      tCtx.arc(l.x, l.y, 4 + glow * 2, 0, Math.PI * 2);
      tCtx.fill();
      tCtx.restore();
    });

    clickWave *= 0.92;

    tCtx.shadowBlur = 0;
    tCtx.fillStyle = '#6b3e1e';
    tCtx.fillRect(205, 450, 30, 80);

    tCtx.save();
    tCtx.shadowBlur = 30;
    tCtx.shadowColor = 'gold';
    tCtx.fillStyle = 'gold';
    tCtx.beginPath();
    tCtx.arc(220, 40, 10, 0, Math.PI * 2);
    tCtx.fill();
    tCtx.restore();
  }

  function animate(t) {
    if (isVertical) {
      drawVerticalGarland(t);
    } else {
      drawHorizontalGarland(t);
    }
    drawTree(t);
    requestAnimationFrame(animate);
  }
  animate(0);
});
</script>

</body>
</html>

Код шаблона без IP и текста И готов к демонстрации

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>🎄 Новый год</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />

<style>
body {
  margin: 0;
  min-height: 220vh;
  font-family: 'Segoe UI', Tahoma, sans-serif;
  background: radial-gradient(circle at top, #0b2a55, #000814 70%);
  color: #fff;
  text-align: center;
  overflow-x: hidden;
}

/* ===== ВЕРХНИЕ ГИРЛЯНДЫ ===== */
#garlandsTop {
  position: fixed;
  top: 0; left: 0;
  width: 100%;
  height: 200px;
  pointer-events: none;
  z-index: 50;
 transition: none;
}

.hero {
  padding-top: 140px;
}

@media (min-width: 1920px) {
  .hero {
    padding-top: 180px;
  }
}

@media (max-width: 480px) {
  #garlandsTop {
    width: 80px !important;
    height: 100vh !important;
    top: 0;
    left: 0;
  }
  .hero {
    padding-left: 100px;
  }
  .time {
    gap: 10px;
    flex-wrap: wrap;
    justify-content: center;
  }
  .time span {
    min-width: 45%;
    margin-bottom: 6px;
  }
  .time b {
    font-size: 24px;
  }
  .time small {
    font-size: 10px;
  }
}

/* ===== ТАЙМЕР ===== */
.countdown {
  margin-top: 28px;
  font-size: 18px;
  opacity: 0.95;
}
.time {
  display: flex;
  justify-content: center;
  gap: 22px;
  margin-top: 10px;
}
.time span {
  display: flex;
  flex-direction: column;
  min-width: 72px;
}
.time b {
  font-size: 34px;
  background: linear-gradient(90deg, #fff, #ffd86b, #fff);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}
.time small { font-size: 12px; opacity: 0.75; }

/* ===== ЁЛКА ===== */
.canvas-wrap {
  margin: 80px auto 0;
  width: 440px;
  height: 560px;
}
#tree {
  width: 100%;
  height: 100%;
  cursor: pointer;
}

/* ===== СНЕГ ===== */
.snow {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 1;
}
.snow span {
  position: absolute;
  top: -10px;
  width: 6px; height: 6px;
  background: #fff;
  border-radius: 50%;
  animation: fall linear infinite;
}
@keyframes fall {
  to { transform: translateY(240vh); }
}
@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
  }
}
</style>
</head>

<body>

<canvas id="garlandsTop"></canvas>

<div class="hero">

  <div class="countdown">
    До Нового года осталось:
    <div class="time">
      <span><b id="d">0</b><small>дней</small></span>
      <span><b id="h">0</b><small>часов</small></span>
      <span><b id="m">0</b><small>мин</small></span>
      <span><b id="s">0</b><small>сек</small></span>
    </div>
  </div>

  <div class="canvas-wrap">
    <canvas id="tree" width="440" height="560"></canvas>
  </div>
</div>

<div class="snow" id="snow"></div>

<script>
document.addEventListener('DOMContentLoaded', () => {
  const gCanvas = document.getElementById('garlandsTop');
  if (!gCanvas || !gCanvas.getContext) {
    if (gCanvas) {
      gCanvas.outerHTML = "<p style='color:#fff; text-align:center; padding:20px;'>Ваш браузер не поддерживает canvas. Обновите браузер для корректного отображения.</p>";
    }
    return;
  }

  const SNOW_COUNT = 170;
  const FIREWORK_CHANCE = 0.05;

  /* ===== СНЕГ ===== */
  const snow = document.getElementById('snow');
  for (let i = 0; i < SNOW_COUNT; i++) {
    const s = document.createElement('span');
    s.style.left = Math.random() * 100 + 'vw';
    s.style.animationDuration = 10 + Math.random() * 15 + 's';
    s.style.transform = `scale(${Math.random() + 0.3})`;
    snow.appendChild(s);
  }

  /* ===== ФЕЙЕРВЕРК ===== */
  class Firework {
    constructor(x, y, ctx) {
      this.x = x;
      this.y = y;
      this.ctx = ctx;
      this.particles = [];
      for (let i = 0; i < 100; i++) {
        this.particles.push({
          x: this.x,
          y: this.y,
          vx: (Math.random() - 0.5) * 5,
          vy: (Math.random() - 0.5) * 5,
          alpha: 1,
          radius: 2 + Math.random() * 2,
        });
      }
    }
    update() {
      this.particles.forEach(p => {
        p.x += p.vx;
        p.y += p.vy;
        p.vy += 0.05;
        p.alpha -= 0.015;
      });
      this.particles = this.particles.filter(p => p.alpha > 0);
    }
    draw() {
      const ctx = this.ctx;
      this.particles.forEach(p => {
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
        ctx.fillStyle = `rgba(255,${Math.floor(200 * p.alpha)},0,${p.alpha})`;
        ctx.shadowColor = `rgba(255,${Math.floor(200 * p.alpha)},0,${p.alpha})`;
        ctx.shadowBlur = 10;
        ctx.fill();
      });
    }
    done() {
      return this.particles.length === 0;
    }
  }

  function showFireworks() {
    const cw = window.innerWidth;
    const ch = window.innerHeight;
    const canvas = document.createElement('canvas');
    canvas.style.position = 'fixed';
    canvas.style.left = 0;
    canvas.style.top = 0;
    canvas.width = cw;
    canvas.height = ch;
    canvas.style.zIndex = 1000;
    document.body.appendChild(canvas);
    const ctx = canvas.getContext('2d');

    const fireworks = [];
    function loop() {
      ctx.clearRect(0, 0, cw, ch);
      if (Math.random() < FIREWORK_CHANCE) {
        fireworks.push(new Firework(Math.random() * cw, Math.random() * ch / 2, ctx));
      }
      fireworks.forEach(fw => {
        fw.update();
        fw.draw();
      });
      for (let i = fireworks.length - 1; i >= 0; i--) {
        if (fireworks[i].done()) {
          fireworks.splice(i, 1);
        }
      }
      requestAnimationFrame(loop);
    }
    loop();
  }

  /* ===== ТАЙМЕР ===== */
  const d = document.getElementById('d');
  const h = document.getElementById('h');
  const m = document.getElementById('m');
  const s = document.getElementById('s');

  function nextNewYear() {
    const now = new Date();
    let y = now.getFullYear();
    if (now >= new Date(y, 0, 1)) y++;
    return new Date(y, 0, 1, 0, 0, 0);
  }

  const eventTime = nextNewYear();

  function updateTimer() {
    const diff = eventTime - new Date();
    if (diff <= 0) {
      document.querySelector('.countdown').innerHTML = '<h2 style="font-size: 48px; color: gold; text-shadow: 0 0 10px #ff0, 0 0 20px #ff0;">С Новым годом!</h2>';
      clearInterval(timerInterval);
      showFireworks();
      return;
    }
    d.textContent = Math.floor(diff / 86400000);
    h.textContent = Math.floor(diff / 3600000) % 24;
    m.textContent = Math.floor(diff / 60000) % 60;
    s.textContent = (Math.floor(diff / 1000) % 60).toString().padStart(2, '0');
  }

  const timerInterval = setInterval(updateTimer, 1000);
  updateTimer();

  /* ===== ГИРЛЯНДА (CANVAS) ===== */
  const gCtx = gCanvas.getContext('2d');
  let isVertical = window.matchMedia("(max-width:480px)").matches;

  function resizeGarlands() {
    if (isVertical) {
      gCanvas.width = 80;
      gCanvas.height = window.innerHeight;
    } else {
      gCanvas.width = window.innerWidth;
      gCanvas.height = 200;
    }
  }
  resizeGarlands();

  let resizeTimeout;
  window.addEventListener('resize', () => {
    clearTimeout(resizeTimeout);
    resizeTimeout = setTimeout(() => {
      isVertical = window.matchMedia("(max-width:480px)").matches;
      resizeGarlands();
    }, 150);
  });

  const lightsCount = 80;
  const topLights = new Array(lightsCount).fill(0).map((_, i) => ({
    t: i / lightsCount,
    hue: Math.random() * 360
  }));
  const verticalLights = new Array(lightsCount).fill(0).map((_, i) => ({
    t: i / lightsCount,
    hue: Math.random() * 360,
    phase: Math.random() * Math.PI * 2
  }));

  function drawHorizontalGarland(t) {
    gCtx.clearRect(0, 0, gCanvas.width, gCanvas.height);
    const baseY = gCanvas.height / 2;
    const amplitude = 25;
    gCtx.strokeStyle = 'rgba(255,255,255,0.25)';
    gCtx.lineWidth = 3;
    gCtx.beginPath();
    for (let x = 0; x <= gCanvas.width; x += 20) {
      const y = baseY + Math.sin(x * 0.01) * amplitude;
      gCtx.lineTo(x, y);
    }
    gCtx.stroke();

    topLights.forEach((l, i) => {
      const x = l.t * gCanvas.width;
      const y = baseY + Math.sin(x * 0.01) * amplitude;
      const wave = (Math.sin(t * 0.003 + i * 0.4) + 1) / 2;

      gCtx.beginPath();
      gCtx.fillStyle = `hsla(${l.hue}, 100%, 70%, ${0.4 + wave * 0.6})`;
      gCtx.shadowColor = `hsla(${l.hue}, 100%, 70%, 1)`;
      gCtx.shadowBlur = 15 + wave * 25;
      gCtx.arc(x, y, 7 + wave * 3, 0, Math.PI * 2);
      gCtx.fill();
    });
  }

  function drawVerticalGarland(t) {
    gCtx.clearRect(0, 0, gCanvas.width, gCanvas.height);
    const baseX = gCanvas.width / 2;
    const amplitude = 15;
    gCtx.strokeStyle = 'rgba(255,255,255,0.25)';
    gCtx.lineWidth = 3;
    gCtx.beginPath();
    for (let y = 0; y <= gCanvas.height; y += 20) {
      const x = baseX + Math.sin(y * 0.02 + t * 0.002) * amplitude;
      gCtx.lineTo(x, y);
    }
    gCtx.stroke();

    verticalLights.forEach((l, i) => {
      const y = l.t * gCanvas.height;
      const x = baseX + Math.sin(y * 0.02 + t * 0.002 + l.phase) * amplitude;
      const wave = (Math.sin(t * 0.005 + i * 0.6) + 1) / 2;

      gCtx.beginPath();
      gCtx.fillStyle = `hsla(${l.hue}, 100%, 70%, ${0.5 + wave * 0.5})`;
      gCtx.shadowColor = `hsla(${l.hue}, 100%, 70%, 1)`;
      gCtx.shadowBlur = 15 + wave * 20;
      gCtx.arc(x, y, 8 + wave * 4, 0, Math.PI * 2);
      gCtx.fill();
    });
  }

  /* ===== ЁЛКА (CANVAS) ===== */
  const tCanvas = document.getElementById('tree');
  const tCtx = tCanvas.getContext('2d');

  const branchPositions = [];
  for (let y = 60; y < 440; y += 6) {
    const w = ((y - 60) / 380) * 180;
    for (let i = 0; i < 6; i++) {
      branchPositions.push({
        x: 220 + (Math.random() - 0.5) * w,
        y: y,
      });
    }
  }

  const treeLights = new Array(45).fill(0).map(() => ({
    x: 220 + (Math.random() - 0.5) * 160,
    y: 100 + Math.random() * 300,
    phase: Math.random() * Math.PI * 2
  }));

  let clickWave = 0;
  tCanvas.addEventListener('click', () => clickWave = 1);

  function drawTree(t) {
    tCtx.clearRect(0, 0, 440, 560);

    tCtx.fillStyle = 'hsl(145,45%,25%)';
    branchPositions.forEach(({x, y}) => {
      tCtx.fillRect(x, y, 2, 8);
    });

    treeLights.forEach(l => {
      const glow = (Math.sin(t * 0.003 + l.phase) + 1) / 2 + clickWave;

      tCtx.save();
      tCtx.fillStyle = `rgba(255,220,150,${0.4 + glow * 0.5})`;
      tCtx.shadowColor = 'rgba(255,220,150,1)';
      tCtx.shadowBlur = 10 + glow * 20;
      tCtx.beginPath();
      tCtx.arc(l.x, l.y, 4 + glow * 2, 0, Math.PI * 2);
      tCtx.fill();
      tCtx.restore();
    });

    clickWave *= 0.92;

    tCtx.shadowBlur = 0;
    tCtx.fillStyle = '#6b3e1e';
    tCtx.fillRect(205, 450, 30, 80);

    tCtx.save();
    tCtx.shadowBlur = 30;
    tCtx.shadowColor = 'gold';
    tCtx.fillStyle = 'gold';
    tCtx.beginPath();
    tCtx.arc(220, 40, 10, 0, Math.PI * 2);
    tCtx.fill();
    tCtx.restore();
  }

  function animate(t) {
    if (isVertical) {
      drawVerticalGarland(t);
    } else {
      drawHorizontalGarland(t);
    }
    drawTree(t);
    requestAnimationFrame(animate);
  }
  animate(0);
});
</script>

</body>
</html>